Skip to content

Reduce MutableProxy per-element overhead and cache per-field proxies#6740

Open
Alek99 wants to merge 4 commits into
claude/reflex-perf-optimizations-01l7a3-eng-10093from
claude/reflex-perf-optimizations-01l7a3-eng-10094
Open

Reduce MutableProxy per-element overhead and cache per-field proxies#6740
Alek99 wants to merge 4 commits into
claude/reflex-perf-optimizations-01l7a3-eng-10093from
claude/reflex-perf-optimizations-01l7a3-eng-10094

Conversation

@Alek99

@Alek99 Alek99 commented Jul 10, 2026

Copy link
Copy Markdown
Member

Linear: ENG-10094

Description

  • MutableProxy._wrap_recursive walked 5 stack frames (_is_called_from_dataclasses_internal) for every element retrieved through a proxy, before the cheap is_mutable_type check that usually decides nothing needs wrapping. The checks are reordered so immutable elements (the common case: ints, strings in a proxied list) skip the frame walk entirely. The dataclasses-internal check still runs for mutable values, preserving dataclasses.asdict/astuple behavior.
  • BaseState._get_attribute constructed a fresh MutableProxy on every read of a mutable state var. The proxy is now cached per instance and field name, invalidated by identity when the underlying value is reassigned. The cache is excluded from pickling (__getstate__), and copy/deepcopy already round-trip through __getstate__, so copies never share or carry proxies.

One deliberate nuance: when _wrap_recursive receives an already-proxied value and is called from dataclasses internals, it now returns the unwrapped value (previously the still-wrapped proxy), matching the documented intent of that path.

Benchmarks (GitHub Actions runner, run, 2 passes each)

Case main this PR speedup
iterate 1000-int list via proxy 0.977 / 0.973 ms 0.219 / 0.214 ms ~4.5x
index 1000 elements via proxy 1.204 / 1.171 ms 0.396 / 0.381 ms ~3.0x
read mutable var attribute 2.82 / 2.87 us 1.17 / 1.15 us ~2.4x

cProfile (20 iterations + 2000 attr reads): _is_called_from_dataclasses_internal (20,000 calls, 0.028s cumulative) disappears from the profile; total function calls drop from 168k to 72k.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
    • tests/units/istate/test_proxy.py: proxy reused across reads and invalidated on reassignment; cache never serialized (pickle round-trip rebinds proxies to the restored state); iteration yields plain immutables while mutable elements stay wrapped.
  • Have you successfully ran tests with your changes locally?

🤖 Generated with Claude Code

https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X


Generated by Claude Code

@Alek99
Alek99 requested a review from a team as a code owner July 10, 2026 20:03
@linear-code

linear-code Bot commented Jul 10, 2026

Copy link
Copy Markdown

ENG-10094

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reduces overhead in state proxying, memo compilation, validation, and websocket updates. The main changes are:

  • Cached per-field mutable state proxies with reassignment invalidation.
  • Faster MutableProxy wrapping for immutable values.
  • Deduplicated passthrough memo definitions during compilation.
  • Faster deterministic component hashing and memo tag generation.
  • Shallower diagnostic type checks on hot state paths.
  • Direct websocket update emits with an explicit event-loop yield.
  • Performance news entries and tests for the changed paths.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
reflex/state.py Adds a non-serialized mutable proxy cache and invalidates cached proxies when state fields are reassigned.
reflex/istate/proxy.py Skips dataclasses frame inspection for immutable values while preserving mutable-value wrapping behavior.
packages/reflex-base/src/reflex_base/components/memo.py Computes passthrough memo tags before body construction and reuses matching memo definitions from the compile registry.
reflex/compiler/plugins/memoize.py Moves passthrough memo registration into the shared memo creation path and only rewrites event triggers when a new body is built.
reflex/compiler/utils.py Compiles passthrough memo bodies using the prebuilt children hole instead of swapping children during render.
packages/reflex-base/src/reflex_base/utils/types.py Adds environment-sensitive validation depth for hot-path state type checks.
packages/reflex-base/src/reflex_base/vars/base.py Runs computed-var return-type checks only when cached values are recomputed.
reflex/app.py Replaces the awaited emit task wrapper with a direct emit and an explicit loop tick.

Reviews (5): Last reviewed commit: "Declare _mutable_proxy_cache as a reserv..." | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Jul 10, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by ×3.2

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 25 untouched benchmarks
⏩ 8 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
test_var_access[mutable_list] 71.1 ms 19.1 ms ×3.7
test_var_access[mutable_dict] 88.4 ms 32.6 ms ×2.7

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/reflex-perf-optimizations-01l7a3-eng-10094 (01ec207) with claude/reflex-perf-optimizations-01l7a3-eng-10093 (1b480cb)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7b1bd3c9f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread reflex/state.py Outdated
Comment thread reflex/state.py Outdated
):
# track changes in mutable containers (list, dict, set, etc)
return MutableProxy(wrapped=value, state=self, field_name=name)
cache = super().__getattribute__("__dict__").get("_mutable_proxy_cache")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to the last PR, unclear on why these cache flags are being directly added into the __dict__ and using object.__setattr__ and the like instead of just defining them as reserved fields on the state.

if we define them as real fields on the state, then users will get an exception if they try to reuse the name, instead of internal tracking machinery just getting broken or corrupting user data.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — converted in d6ab47c: _mutable_proxy_cache is now a declared field(default_factory=dict, is_var=False) like _backend_vars/_was_touched, added to RESERVED_BACKEND_VAR_NAMES so a user var with that name can't silently collide. It stays out of pickles (__getstate__ pop) and is recreated empty in __setstate__. I'll apply the same treatment to the propagation-frontier fields in #6743.


Generated by Claude Code

claude added 4 commits July 17, 2026 18:44
_wrap_recursive walked 5 stack frames (dataclasses-internal check) for
every element retrieved through a proxy, even immutable scalars that
never get wrapped. Check is_mutable_type first so immutable elements
skip the frame walk entirely.

Also cache the MutableProxy built for each mutable state var on the
instance, keyed by field name, instead of constructing a fresh proxy
on every attribute read. The cache is invalidated by identity when the
underlying value is reassigned and is excluded from pickling (copy and
deepcopy already go through __getstate__).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
Address review feedback: drop the _mutable_proxy_cache entry when a
base or backend var is assigned, so the replaced value is not kept
alive by the cached proxy until the next read. Update
test_set_base_field_via_setter for the new cached-proxy identity
semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
Address review feedback: instead of managing the proxy cache as an
ad-hoc instance __dict__ entry, declare it as a real (non-var) field
like _backend_vars/_was_touched and add it to
RESERVED_BACKEND_VAR_NAMES, so user vars cannot silently collide with
the framework's tracking machinery. The cache is still excluded from
pickling and recreated empty on unpickle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtmC37Y8kJ5S6
@Alek99
Alek99 force-pushed the claude/reflex-perf-optimizations-01l7a3-eng-10094 branch from d6ab47c to 01ec207 Compare July 18, 2026 01:46
@Alek99
Alek99 changed the base branch from main to claude/reflex-perf-optimizations-01l7a3-eng-10093 July 18, 2026 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants